package org.easymodelers.template.utils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

import org.easymodelers.template.TemplateProcessingException;
import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EFactory;
import org.eclipse.emf.ecore.ENamedElement;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;

public abstract class DynamicModelManager {
	private static String firstUpper(String s) {
		return s.substring(0, 1).toUpperCase() + s.substring(1);
	}
	
	public static EObject createEObject(EFactory factory, String typeName) {
		String createMethodName = "create" + typeName;
		try {
			Method createMethod = factory.getClass().getMethod(createMethodName);
			return (EObject) createMethod.invoke(factory);
		} catch(Exception ex) {
			String message = "Unable to create the type "+typeName+" for the factory "+factory.getClass().getName()
								+" with the method "+createMethodName;
			throw new TemplateProcessingException(message, ex);
		}
	}

	public static boolean isEListAttribute(EObject parent, String name) {
		Class clazz = parent.getClass();
		String getMethodName = "get" + firstUpper(name);
		try {
			Method getMethod = clazz.getMethod(getMethodName);
			Class fieldType = getMethod.getReturnType();

			return Collection.class.isAssignableFrom(fieldType);
		} catch(Exception ex) {
			String message = "Unable if the property "+name+" of the type "+clazz.getName()
								+"is a collection";
			throw new TemplateProcessingException(message, ex);
		}
	}
	
	public static EList getEListAttribute(EObject parent, String name) {
		Class clazz = parent.getClass();
		String getMethodName = "get" + firstUpper(name);
		Method getMethod = null;
		try {
			getMethod = clazz.getMethod(getMethodName);
			return (EList) getMethod.invoke(parent);
		} catch(Exception ex) {
			Class returnType = getMethod.getReturnType();
			String message = "Error during the execution of method "+getMethodName+" for class "+clazz.getName()
					+ " - return: "+returnType.getName();
			throw new TemplateProcessingException(message, ex);
		}
	}
	
	public static EObject getEClassAttribute(EObject o, String name) {
		if (!(o instanceof EClass)) {
			return null;
		}

		EClass c = (EClass) o;
		for (EAttribute attribute : c.getEAllAttributes()) {
			if (attribute.getName().equals(name)) {
				return attribute;
			}
		}
		
		for (EReference reference : c.getEAllReferences()) {
			if (reference.getName().equals(name)) {
				return reference;
			}
		}

		return null;
	}
	
	public static Object getAttribute(EObject o, String name) {
		Class clazz = o.getClass();
		String getMethodName = "get" + firstUpper(name);
		Method getMethod = null;

		try {
			getMethod = clazz.getMethod(getMethodName);
			return getMethod.invoke(o);
		} catch(Exception ex) {
			ex.printStackTrace();
			
			Class returnType = getMethod.getReturnType();
			String message = "Error during the execution of method "+getMethodName+" for class "+clazz.getName()
					+ " - return: "+returnType.getName();
			throw new TemplateProcessingException(message, ex);
		}
	}
	
	private static Object convertParameter(Object value, String type) {
		System.out.println("Trying to convert value "+value+" to type "+type+"...");
		if ("int".equals(type) || "java.lang.Integer".equals(type)) {
			return Integer.parseInt((String)value);
		} else if ("boolean".equals(type) || "java.lang.Boolean".equals(type)) {
			return Boolean.parseBoolean((String)value);
		}
		
		return value;
	}
	
	public static Object setAttribute(EObject o, String name, Object value) {
		Class clazz = o.getClass();
		String setMethodName = "set" + firstUpper(name);
		Method setMethod = null;
		
		try {
			for (Method method : clazz.getMethods()) {
				//System.out.println(" -> method.getName() = "+method.getName()+" - setMethodName = |"+setMethodName+"|");
				if (setMethodName.equals(method.getName())) {
					setMethod = method;
					break;
				}
			}
			
			if (setMethod==null) {
				String message = "Unable to find method "+setMethodName+" for class "+clazz.getName();
				throw new TemplateProcessingException(message);
			}

			Class[] parameterTypes = setMethod.getParameterTypes();
			//System.out.println("converParameter = "+convertParameter(value, parameterTypes[0].getName()));
			return setMethod.invoke(o, convertParameter(value, parameterTypes[0].getName()));
		} catch(Exception ex) {
			if (ex instanceof TemplateProcessingException) {
				throw (TemplateProcessingException)ex;
			}

			Class[] parameterTypes = setMethod.getParameterTypes();
			String message = "Error during the execution of method "+setMethod+" for class "+clazz.getName()
					+ " - expected parameter: "+parameterTypes[0].getName()+", actual parameter: "+o.getClass().getName();
			throw new TemplateProcessingException(message, ex);
		}
	}
	
	public static void setEObject(EObject parent, String name, EObject o) {
		boolean collection = isEListAttribute(parent, name);
		System.out.println("Setting an eobject ("+o.getClass().getName()+") on a parent eobject "+parent.getClass().getName()
				+" with the property "+name+" ("+(collection?"collection":"not collection")+")...");
		//System.out.println();
		String setMethodName = "set" + firstUpper(name);
		Class parentClass = parent.getClass();
		Method setMethod = null;

		if (collection) {
			Collection c = (Collection) getEListAttribute(parent, name);
			c.add(o);
		} else {
			Object parameter = null;
			try {
				for (Method method : parentClass.getMethods()) {
					if (setMethodName.equals(method.getName())) {
						setMethod = method;
						break;
					}
				}
				
				if (setMethod==null) {
					String message = "Unable to find method "+setMethodName+" for class "+parentClass.getName();
					throw new TemplateProcessingException(message);
				}
	
				setMethod.invoke(parent, o);
			} catch(Exception ex) {
				if (ex instanceof TemplateProcessingException) {
					throw (TemplateProcessingException)ex;
				}

				ex.printStackTrace();
				Class[] parameterTypes = setMethod.getParameterTypes();
				String message = "Error during the execution of method "+setMethod+" for class "+parentClass.getName()
										+ " - expected parameter: "+parameterTypes[0].getName()
										+", actual parameter: "+parameter.getClass().getName();
				throw new TemplateProcessingException(message, ex);
			}
		}

	}
	
	public static boolean checkEObject(EObject o, Map<String,String> attributes, Map<String, Object> context) {
		for (String attributeName : attributes.keySet()) {
			String attributeValue = attributes.get(attributeName);
			Object eobjectValue = getAttribute(o, attributeName);
			System.out.println("- attributeName = "+attributeName);
			if (!(eobjectValue instanceof String)) {
				continue;
			}

			System.out.println("  values to compare - attributeValue = "+attributeValue+", eobjectValue = "+eobjectValue);

			if (attributeValue.startsWith("${") && attributeValue.endsWith("}")) {
				attributeValue = attributeValue.replace("${", "");
				attributeValue = attributeValue.replace("}", "");
				attributeValue = (String) ContextManager.getValueFromContext(context, attributeValue);
			}

			System.out.println("  values to compare - attributeValue = "+attributeValue+", eobjectValue = "+eobjectValue);

			if (!attributeValue.equals(eobjectValue)) {
				System.out.println("Not matched!");
				return false;
			}
		}
		System.out.println("Matched!");
		return true;
	}

	public static boolean hasEObjectInList(EList l, Map<String,String> attributes, Map<String, Object> context) {
		for (Iterator i = l.iterator(); i.hasNext(); ) {
			EObject o = (EObject) i.next();
			if (checkEObject(o, attributes, context)) {
				return true;
			}
		}
		return false;
	}

	private static boolean checkEObjectType(EObject o, String type) {
		//System.out.println("> checkEObjectType - o = "+o.getClass().getName()+", type = "+type);
		String oType = o.getClass().getName();
		int index = oType.lastIndexOf(".");
		oType = oType.substring(index+1);
		if (oType.endsWith("Impl")) {
			oType = oType.substring(0, oType.length()-4);
		}
		
		//System.out.println("oType = "+oType);
		return oType.equals(type);
	}
	
	private static boolean checkEObjectName(EObject o, String type) {
		if (!(o instanceof ENamedElement)) {
			return false;
		}

		return ((ENamedElement)o).getName().equals(type);
	}
	
	private static boolean checkEObjectAttribute(EObject o, String fieldName, String fieldValue) {
		//System.out.println("checkEObjectAttribute - fieldName = "+fieldName+", fieldValue = "+fieldValue);
		int index = fieldName.indexOf(".");
		String attributeValue =null;
		if (index!=-1) {
			EObject tmpO = o;
			String remainingFieldName = fieldName;
			while (index!=-1) {
				String o1Name = remainingFieldName.substring(0, index);
				String o1Attribute = remainingFieldName.substring(index+1);
				remainingFieldName = o1Attribute;
				tmpO = (EObject) getAttribute(tmpO, o1Name);
				index = remainingFieldName.indexOf(".");
				if (index==-1) {
					attributeValue = (String) getAttribute(tmpO, o1Attribute);
				} else {
					o1Attribute = remainingFieldName.substring(0, index);
					tmpO = (EObject) getAttribute(tmpO, o1Attribute);
				}
			}
		} else {
			attributeValue = (String) getAttribute(o, fieldName);
		}

		if (attributeValue.equals(fieldValue)) {
			return true;
		}

		return false;
	}
	
	public static EObject findEObjectByClass(EObject rootEObject, String type) {
		return findEObject(rootEObject, type, false);
	}
	
	public static EObject findEObjectByClass(EObject rootEObject, String type, String fieldName, String fieldValue) {
		return findEObject(rootEObject, type, fieldName, fieldValue, false);
	}
	
	public static EObject findEObjectByName(EObject rootEObject, String type) {
		return findEObject(rootEObject, type, true);
	}

	public static EObject findEObjectByName(EObject rootEObject, String type, String fieldName, String fieldValue) {
		return findEObject(rootEObject, type, fieldName, fieldValue, true);
	}

	public static EObject findEObject(EObject rootEObject, String type, boolean nameBased) {
		return findEObject(rootEObject, type, null, null, nameBased);
	}

	public static boolean checkEObject(EObject o, String type, String fieldName, String fieldValue, boolean nameBased) {
		//System.out.println("> checkEObject - fieldName = "+fieldName+", fieldValue = "+fieldValue+", nameBased = "+nameBased);
		boolean noCondition = (fieldName==null || fieldValue==null);
		boolean check = nameBased ? checkEObjectName(o, type) : checkEObjectType(o, type);
		System.out.println("   > check = "+check);
		
		if (check) {
			if (noCondition) {
				return true;
			}

			if (checkEObjectAttribute(o, fieldName, fieldValue)) {
				return true;
			}
		}
		return false;
	}
	
	public static EObject findEObject(EObject rootEObject, String type, String fieldName, String fieldValue, boolean nameBased) {
		//System.out.println("> findEObject - fieldName = "+fieldName+", fieldValue = "+fieldValue);
		for (TreeIterator i = rootEObject.eAllContents(); i.hasNext(); ) {
			EObject o = (EObject) i.next();
			if (checkEObject(o, type, fieldName, fieldValue, nameBased)) {
				return o;
			}
		}
		System.out.println("Unabled to find EObject with type ("+type+") and condition ("+fieldName+" = "+fieldValue+")");
		return null;
	}

	public static EList<EObject> findEObjectsByClass(EObject rootEObject, String type) {
		return findEObjects(rootEObject, type, false);
	}
	
	public static EList<EObject> findEObjectsByClass(EObject rootEObject, String type, String fieldName, String fieldValue) {
		return findEObjects(rootEObject, type, fieldName, fieldValue, false);
	}
	
	public static EList<EObject> findEObjectsByName(EObject rootEObject, String type) {
		return findEObjects(rootEObject, type, true);
	}

	public static EList<EObject> findEObjectsByName(EObject rootEObject, String type, String fieldName, String fieldValue) {
		return findEObjects(rootEObject, type, fieldName, fieldValue, true);
	}

	public static EList<EObject> findEObjects(EObject rootEObject, String type, boolean nameBased) {
		return findEObjects(rootEObject, type, null, null, nameBased);
	}

	public static EList<EObject> findEObjects(EObject rootEObject, String type, String fieldName, String fieldValue, boolean nameBased) {
		//System.out.println("> findEObject - fieldName = "+fieldName+", fieldValue = "+fieldValue);
		EList<EObject> list = new BasicEList<EObject>();

		boolean noCondition = (fieldName==null || fieldValue==null);

		for (TreeIterator i = rootEObject.eAllContents(); i.hasNext(); ) {
			EObject o = (EObject) i.next();
			Class clazz = o.getClass();
			
			boolean check = nameBased ? checkEObjectName(o, type) : checkEObjectType(o, type);
			
			if (check) {
				if (noCondition) {
					list.add(o);
					break;
				}

				if (checkEObjectAttribute(o, fieldName, fieldValue)) {
					list.add(o);
				}
			}
		}
		System.out.println("Unabled to find EObjects with type ("+type+") and condition ("+fieldName+" = "+fieldValue+")");
		return list;
	}

	public static Object getEnumValue(String type, String fieldName, String[] enumPackages) {
		System.out.println("Evaluating enum value for "+type+"."+fieldName+"...");
		for (String enumPackage : enumPackages) {
			try {
				String completeType = enumPackage+"."+type;
				Class clazz = Class.forName(completeType);
				Field field = clazz.getField(fieldName);
				return field.get(null);
			} catch(Exception ex) {
			}
		}
		
		String message = "Unable to get the enum value for "+type+"."+fieldName+"...";
		throw new TemplateProcessingException(message);
	}

	public static Object getEnumValue(String type, String fieldName) {
		System.out.println("Evaluating enum value for "+type+"."+fieldName+"...");
		try {
			//TODO: generalize
			//type = "org.eclipse.gmf.gmfgraph."+type;
			Class clazz = Class.forName(type);
			Field field = clazz.getField(fieldName);
			return field.get(null);
		} catch(Exception ex) {
			String message = "Unable to get the enum value for "+type+"."+fieldName+"...";
			throw new TemplateProcessingException(message, ex);
		}
	}
}
